/* Copyright Andy Mitchell 2007. No warranties */

/*

Two main purposes:
1)
Parse the Gmail Labels box, re-arranging the elements, and extracting all Labels in the account.
Provide functions to retrieve those labels (as new DOM element, array, hash, etc.)

2)
Provide queued interaction with the 'More Actions' drop-down box. 
Used to apply/remove labels to a thread; get current labels on a thread; etc.

*/

function Labels(gtdinboxPage){
	this.gtdinboxPage = gtdinboxPage;
	this.doc = gtdinboxPage.doc;
	this.settings = gtdinboxPage.settings;
	this.eventObservers = new EventObservers(this);
	this.eventHandlers = new EventHandlers_Mutator(this);
	this._showHiders = [];
	this._multiToOne = new MultiToOne(gtdinboxPage, 100);
	
	this._labelsEls = {};
//	this._labels = null;
	this._labels = {};
	this._labelsHash = null;
	this._labelsArray = null;
	this._labelObjectsHash = null;
	
	this._labelQueue = [];
	
	this._tmpLabelNames = [];
};

Labels.prototype.refresh = function(event){
	// Called by DOM
	// TODO add a multiToOne here
	this._tmpLabelNames.length = 0;
	this.load();
};

Labels.prototype.load = function(elNav, tmpLabelNames){
	// Parse the labels box; and re-arrange it at the same time
	
try{
	this._lastRefresh = (new Date()).valueOf();
	
	// Retrieve the label box
	if( elNav && elNav.nodeType==1 ){
		this.elNav = elNav;
	}
	var result = this.doc.evaluate( "//div[@class='XoqCub XPj4ef']//div[@class='XoqCub C9Pn8c']//div[@class='R7iiN c1norb']//div[@class='nb1Q2b']//div[@class='pvSW6e']", this.elNav, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
	this.elBox = result.singleNodeValue;
	if( !tmpLabelNames && this.elBox.firstChild.className.indexOf("showhider")>-1 ){	// already processed -> stop
		// Just check each entry to see if changed back to prefixed form (& correct it)
		var result = this.doc.evaluate( "//div[@class='XoqCub XPj4ef']//div[@class='XoqCub C9Pn8c']//div[@class='R7iiN c1norb']//div[@class='nb1Q2b']//div[@class='pvSW6e']//div[starts-with(@class,'showhider')]//table//span[@class='zD5BAe']", this.elNav, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
		for( var i = 0; i < result.snapshotLength; i++ ){
			var el = result.snapshotItem(i);
			for( t in this.settings.labels.types ){
				if( el.innerHTML.indexOf(this.settings.labels.types[t].prefix)==0 ){
					el.innerHTML = el.innerHTML.replace(this.settings.labels.types[t].prefix, "");
				}
			}
		}
		return;
	}
	
	// Stop recursiveness
	if( this.elObserved ) this.eventObservers.stopObserving( this.elObserved, "DOMNodeInserted", this.refresh, false);
	if( this.elObserved ) this.eventObservers.stopObserving( this.elObserved, "mousedown", this.mouseDown, true);
	
	// Destroy old show hiders
	if( !tmpLabelNames ){
		for( var i = 0; i < this._showHiders.length; i++ ){
			this._showHiders[i].destroy();
		}
		this._showHiders.length = 0;
	}
	this._labels = {};
	this._labelsEls = {};
	this._labelsHash = {"_all":{}};
	this._labelsArray = {"_all":[], "_all-misc":[]};
	this._labelObjectsHash = {};

	// Grab the actual original table (contains each label as row)
	var result = this.doc.evaluate( "//div[@class='XoqCub XPj4ef']//div[@class='XoqCub C9Pn8c']//div[@class='R7iiN c1norb']//div[@class='nb1Q2b']//div[@class='pvSW6e']//table[last()]", this.elNav, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
	this.elTable = result.singleNodeValue;

	// Extract labels
	var elRows = this.elBox.getElementsByTagName("TR");
	for( var i = 0; i < elRows.length; i++ ){	// Get each label row
		var elRow = elRows[i];
		var elName = elRow.getElementsByTagName("SPAN")[0];
		var labelname = elName.getAttribute("gtdLabelName");
		if( !labelname ) labelname = elName.innerHTML.replace(/\s\(\d+\)$/, "");
		this.processNewLabel(this._labels, labelname, elRow, elName);
	}
	
	// Perform a first pass on the labels
	if( tmpLabelNames ){
	  for( var i = 0; i < tmpLabelNames.length; i++ ){
	    this.processNewLabel(this._labels, tmpLabelNames[i].labelname, tmpLabelNames[i].elRow, tmpLabelNames[i].elName);
	  }
	}

	// Perform second pass on labels
	// This pass refined them (what a dumb name), adding some extra attributes, etc...
	if( !this.settings.showLabelTreeNodes ) this.settings.showLabelTreeNodes = {};
  for( t in this._labels ){
	  for( n in this._labels[t] ){
	    this.refineNestedLabels(this._labels[t][n]);
		}
  }
	
	// Build new DOM elements - create a label group for each type; wrap into a show hider; and insert in front of original table
	for( t in this.settings.labels.types ){
		var elLabelsBox = this.doc.createElement("DIV");
			elLabelsBox.className = "labels-group";
			var displayName = this.settings.labels.types[t].name || t;
			elLabelsBox.innerHTML = "<div class='title'>" + displayName + "</div>";
		var elLabels = this.doc.createElement("DIV");
			elLabels.className = "labels";
		elLabelsBox.appendChild( elLabels );
			
		
		if( this._labels[t] ){
			var elT = this.doc.createElement("TABLE");
			  // NEED TO MOVE THESE STYLE SETTINGS TO THE CSS FILE (PJT-23/01/2008)
			  //elT.style.tableLayout = "fixed"; // THIS LINE IS CAUSING THE COLOR SQUARES TO SMUSH TO THE LEFT
			  //elT.style.marginBottom = "0px";
			var elBody = this.doc.createElement("TBODY");
			  elBody.className = "showhider-tbody";
	      this.eventObservers.observe( elBody, "mousedown", this.labelsSectionMousedown, false);
			elT.appendChild(elBody);
			
			if( this.settings.labels.types[t].defaults ){	// process in order
				for( d in this.settings.labels.types[t].defaults ){
					var n = this.settings.labels.types[t].defaults[d];
					// If t==Status & d==Finished, perhaps do something more signficant
					if( n && this._labels[t][n] ){
						this.spawnFromNestedLabels(elBody, elLabels, this._labels[t][n], tmpLabelNames);
					}
				}
			}

			for( n in this._labels[t] ){
				this.spawnFromNestedLabels(elBody, elLabels, this._labels[t][n], tmpLabelNames);
			}
			
			if( !tmpLabelNames ){
				var sh = new Showhider(this.gtdinboxPage, t, {doc:this.doc, elParent:this.elBox, elBefore:this.elTable, elContent:elT, title:t} );
					sh.elTitle.style.color = "black";
					sh.elTitle.style.fontWeight = "bold";
				this._showHiders.push( sh );
			}
		}

		this._labelsEls[t] = elLabelsBox;
	}
	
// ************************************************************************
// ************************************************************************
// ************************************************************************
// ***                                                                  ***
// *** *** *** *** *** *** *** THIS IS OLD CODE *** *** *** *** *** *** ***
// ***                                                                  ***
// *** HOWEVER - Something like this DOES eventually need to be here... ***
// ************************************************************************
// ************************************************************************
// ************************************************************************

	// Run through the labels and collapse everything that should be
//  for( labelfullname in this._labelObjectsHash ){
//    var label = this._labelObjectsHash[labelfullname];
//    if( label._isCollapsed ) label.childrenCollapsed(true);
//  } 

	this.elObserved = this.elBox.parentNode.parentNode.parentNode;
	this.eventObservers.observe( this.elObserved, "DOMNodeInserted", this.refresh, false);
	this.eventObservers.observe( this.elObserved, "mousedown", this.mouseDown, true);
	

}catch(e){
	if( GTDDEBUGMODE ) alert("labels load\n" + e.toString());
}
try{	
	this.eventHandlers.broadcast("refresh-labels");
}catch(e){
	if( GTDDEBUGMODE ) alert("labels load 2\n" + e.toString());
}
};

/************************************************************************************
 ** processNewLabel - process a single, raw label                                  **
 **   return value: none                                                           **
 **                                                                                **
 ** labels     hash    created label objects will be stored in this hash           **
 ** labelname  string  original label name as extracted from the gmail label       **
 ** elRow      TR      table row element containing gmail label                    **
 ** elName     SPAN    span element containing label                               **
 ************************************************************************************/
Labels.prototype.processNewLabel = 	function(labels, labelname, elRow, elName){
try{
	var foundType = null;
	var noPrefixLabel = labelname;
		
	elName.setAttribute("gtdLabelName", labelname);
	for( t in this.settings.labels.types ){
		if( labelname.indexOf(this.settings.labels.types[t].prefix)==0 ){
			foundType = t;
			break;
		}
	}
	if( !foundType ) foundType = "Misc";
	if(  foundType ){
		if( !labels[foundType] ) labels[foundType] = {};
		if( labelname.indexOf( this.settings.labels.types[foundType].prefix ) == 0 ){
		  noPrefixLabel = labelname.replace(this.settings.labels.types[foundType].prefix, "");
		}
		/*
		if( foundType=="Old" ){
			for( t2 in this.settings.labels.types ){
				if( this.settings.labels.types[t2].prefix && noPrefixLabel.indexOf(this.settings.labels.types[t2].prefix)==0 ){
					noPrefixLabel = noPrefixLabel.replace(this.settings.labels.types[t2].prefix, "");
					break;
				}
			}
		}
		*/
		var subs = noPrefixLabel.split("/");		
		var curLabelname = ( noPrefixLabel != labelname ? this.settings.labels.types[foundType].prefix : "") + subs[0];
		
		this.parseNestedLabels( foundType, labels[t], subs, labelname, elRow, elName, curLabelname );
		this._labelsHash[labelname] = foundType;
		
		/*
		var elFilterPopup = this.doc.createElement("IMG");
			elFilterPopup.className = "filter-popuper";
		elFilterPopup.src = "chrome://gtdinbox/content/filter-extend.png";
		elName.parentNode.appendChild(elFilterPopup);
		*/
	}
	if( !this._labelsArray[foundType] ) this._labelsArray[foundType] = [];
	if( !this._labelsHash[foundType] ) this._labelsHash[foundType] = {};
	this._labelsHash[foundType][labelname] = true;
	this._labelsHash["_all"][labelname] = true;
	this._labelsArray[foundType].push(labelname);
	this._labelsArray["_all"].push(labelname);
	if( foundType!="Misc" ) this._labelsArray["_all-misc"].push(labelname);
}catch(e){
	if( GTDDEBUGMODE ) alert("labels processNewLabel\n" + e.toString());
}
};

/************************************************************************************
 ** parseNestedLabels - parse the original gmail labels and create label objects   **
 **   return value: none                                                           **
 **                                                                                **
 ** type          string  type of current label (ie status, project, contect, etc) **
 ** labels        hash    created label objects will be stored in this hash        **
 **                       this hash is specific to this type only                  **
 ** subs          array   array of strings, subdivisions of this label split by /  **
 ** labelname     string  original label name as extracted from the gmail label    **
 ** elRow         TR      table row element containing gmail label                 **
 ** elName        SPAN    span element containing label                            **
 ** depth         int     current nested depth                                     **
 ** curLabelname  string  current label name including all subdivision prefixes    **
 ************************************************************************************/
Labels.prototype.parseNestedLabels = function(type, labels, subs, labelname, elRow, elName, curLabelname, depth){
try{
	if( !depth ) depth = 0;
	var n = subs[0];
	if( typeof curLabelname == "undefined" ) curLabelname = n;
	if( !labels[n] ) {
	  labels[n] = {name:n, children:{}, parent:null, elRow:null, elName:null, 'depth':depth, labelname:curLabelname, labelsourcename:labelname, labelfullname:curLabelname, isPlaceHolder:true, 'type':type, spawnComplete:false};
	  this._labelObjectsHash[labels[n].labelfullname] = labels[n];
	}
	if( subs.length==1 ){	// final one
		labels[n].elRow = elRow;
		elName.innerHTML = elName.innerHTML.replace(/.+?(\s\(\d+\))?$/, n + "$1");
		elName.setAttribute("gtdName", n);
		elName.setAttribute("gtdLabelType", type);
		labels[n].elName = elName;
		labels[n].labelname = curLabelname;
		labels[n].isPlaceHolder = false;
    this._labelObjectsHash[labels[n].labelfullname] = labels[n];
	}else{
		subs.splice(0,1);
		this.parseNestedLabels( type, labels[n].children, subs, labelname, elRow, elName, curLabelname + '/' + subs[0], depth+1);
	}
}catch(e){
	if( GTDDEBUGMODE ) alert("labels parseNestedLabels\n" + e.toString());
}
};

/************************************************************************************
 ** refineNestedLabels - add a couple extra require attributes to the labels tree  **
 **   return value: none                                                           **
 **                                                                                **
 ** elToggleLabel  TR / DIV   element which is being toggled                       **
 ** elTreeImage    IMG        target image element of the mousedown event          **
 ************************************************************************************/
Labels.prototype.refineNestedLabels = function(label, parent){
try{
  isCollapsedSetting = ( this.settings.showLabelTreeNodes.rememberOpenClose ? this.settings.showLabelTreeNodes[label.labelfullname] : false );
  label._isCollapsed = ( typeof isCollapsedSetting == "undefined" ? false : isCollapsedSetting ); 
  label.parent  = ( parent ? parent : null );
  
  if( typeof label.hasChildren == "undefined" ) label.hasChildren = false;
  for( c in label.children ){
    label.hasChildren = true;
    this.refineNestedLabels(label.children[c], label);
  }

// ************************
// *** THIS IS OLD CODE ***
// ************************
//  label.childrenCollapsed = GIPROTOTYPE.bind(label, function(v, doNotSaveStatus){
//    if( typeof doNotSaveStatus == "undefined" ) doNotSaveStatus = false;
//    
//    // First, if we don't have a value for v, then just return this labels collapse status 
//    if( typeof v == "undefined" ) return label._isCollapsed;
//    
//    // Next, act upon the provided (or not) value of doNotSaveStatus
//    if( !doNotSaveStatus ) label._isCollapsed = v;
//    else                   label.elRow.style.display = ( v ? 'none' : '' );
//
//    // Finally, recurse for each child
//    for( c in label.children ){
//      label.children[c].childrenCollapsed( (v || label._isCollapsed), true );
//    }
//  });  
}catch(e){
	if( GTDDEBUGMODE ) alert("labels refineNestedLabels\n" + e.toString());
}
};

/************************************************************************************
 ** spawnFromNestedLabels - recursively display a label and children               **
 **   return value: none                                                           **
 **                                                                                **
 ** elBody         TBODY  labels will be appended to this table body element       **
 ** elLabels       DIV    *** ADD DESCRIPTION HERE ***                             **
 ** label          obj    label object we are currently working on                 **
 ** tmpLabelNames  ???    Only defined when called by addNewLabel - MORE DETAILS!! **
 ** depth          int    Current nested depth                                     **
 ************************************************************************************/
Labels.prototype.spawnFromNestedLabels = function(elBody, elLabels, label, tmpLabelNames, depth){
try{
	if( !depth ) depth = 0;
	
	// Only spawn each label once
	if( label.spawnComplete ) return;
	
	// Prepare the label
	if( label.elRow ){
		label.elName.setAttribute("gtdLabelName", label.labelname);
	}else{
	  // This is not a real label - create a placeholder (i.e. label.elRow is not defined)
    var elTableCell       = this.doc.createElement("TD");
		elTableCell.innerHTML = "<div class=yyT6sf><span>" + label.name + "</span></div>";

    label.elRow           = this.doc.createElement("TR");
    label.elRow.className = "placeholder-label";
    label.elRow.appendChild(elTableCell);    
    label.elName = elTableCell.firstChild.firstChild;
	}
  label.elRow.setAttribute("gtdLabelFullName", label.labelfullname);
  label.elName.parentNode.style.paddingLeft = (label.depth*12) + "px";
  label.elName.parentNode.style.marginLeft = "12px";
  
  var imageSrcPrefix = "chrome://gtdinbox/content/collapsetree-";
  var elTreeImage = this.doc.createElement("IMG");
	  //elTreeImage.style.marginLeft = (label.depth * 12) + "px";
	  elTreeImage.setAttribute("gtdLabelFullName", label.labelfullname);
    elTreeImage.className = "label-folder-toggle";

  if ( label.hasChildren ) {
    elTreeImage.alt = (label._isCollapsed ? '+' : '-' );
    elTreeImage.src = imageSrcPrefix + (label._isCollapsed ? 'closed' : 'open') + ".gif";
  } else {
    elTreeImage.src = imageSrcPrefix + "leaf.png";
    elTreeImage.className += "-leaf";
  }

	if( !tmpLabelNames ) {
	  // Retrieve DIV tank containing the label
    var elLabelDiv = GIPROTOTYPE.getElementsByClassName( label.elRow, "yyT6sf")[0];

    if( typeof elLabelDiv != "undefined" ) {      
      // Place the toggle (open/closed/leaf) image at the front of the label DIV
      elLabelDiv.insertBefore(elTreeImage, elLabelDiv.firstChild);
    }
    
    elBody.appendChild( label.elRow );
  }
	
	var elTreeImg = elTreeImage.cloneNode(true);	// create one for this label
	var elLabel = this.doc.createElement("DIV");
		elLabel.innerHTML = label.name;
		elLabel.style.paddingLeft = (label.depth*12) + "px";
    elLabel.className = "placeholder-label";
	  
	  
	  elLabel.insertBefore(elTreeImg, elLabel.firstChild);
		elLabel.setAttribute("gtdName", label.name);
		elLabel.setAttribute("gtdType", label.type);
		elLabel.setAttribute("gtdLabelName", (label.isPlaceHolder? "" : label.labelname) );
	  elLabel.setAttribute("gtdLabelFullName", label.labelfullname);
		elLabel.setAttribute("gtdLabelSourceName", label.labelsourcename);	// used so even in nesting, can detect original
		elLabel.setAttribute("gtdLabelParentName", "" + label.labelname.substring(0, label.labelname.lastIndexOf('/')));
		if( depth==0 ) elLabel.setAttribute("gtdRootLabel", "1");
		elLabel.className = (label.isPlaceHolder? "placeholder-label" : "label");
	label.elDefaultLabel = elLabel;
	elLabels.appendChild( elLabel );

  // Finally, display labels for all children (children of childre, etc)
	for( c in label.children ){
		this.spawnFromNestedLabels(elBody, elLabels, label.children[c], tmpLabelNames, depth+1);
	}
	
	// Set Collapsable Visual Default (if top level)
	if( label.hasChildren && !label.parent ){
		this.toggleLabelTreeNode( label.elRow, elTreeImage, true, label );
		this.toggleLabelTreeNode( elLabel, elTreeImg, true, label );
	}
	

  // Lastly, flag that spawn has completed for this label
  label.spawnComplete = true;
  
}catch(e){
	if( GTDDEBUGMODE ) alert("Labels spawnFromNestedLabels\n" + e.toString());
}
};

/************************************************************************************
 ** labelsSectionMousedown - event handler for mousedown of a label section        **
 **   return value: none                                                           **
 **                                                                                **
 ** event          event  event object passed to the function
 ************************************************************************************/
Labels.prototype.labelsSectionMousedown = function(event){
  var elTreeImage = GIPROTOTYPE.element(event);
  
  if( elTreeImage.className == "label-folder-toggle" ){
    var elInspected = elTreeImage;
    while( elInspected.parentNode ){
      elInspected = elInspected.parentNode;
      if (( elInspected.className == 'WnjAod') || ( elInspected.className == 'placeholder-label' ) || (elInspected.className == 'label' )){
        this.toggleLabelTreeNode( elInspected, elTreeImage );
        break;
      }
    }
  }
}

/************************************************************************************
 ** toggleLabelTreeNode - toggle (collapse or uncollapse) a label tree node        **
 **   return value: none                                                           **
 **                                                                                **
 ** elToggleLabel  TR / DIV   element which is being toggled                       **
 ** elTreeImage    IMG        target image element of the mousedown event          **
 ************************************************************************************/
Labels.prototype.toggleLabelTreeNode = function( elToggleLabel, elTreeImage, overrideIsCollapsed, toggleLabel ){
try{
  if( !toggleLabel && elToggleLabel ) toggleLabel = this._labelObjectsHash[ elToggleLabel.getAttribute("gtdLabelFullName") ];
  
  // **************************************************************************
  // *** TODO ON THE COLLAPSIBLE LABELS TASK
  // ***
  // *** First, saved state gas not been at all added into this new function.
  // ***
  // *** Second, the initial implementation only had to save state for one
  // *** instance of the label tree. Now we have multiple instances and need
  // *** to represent that within settings so that they each have their own
  // *** state of collapsed / non-collapsed nodes.
  // *** 
  // *** Last, a pass should be made on the code to make sure that all
  // *** remnants of the first implemention have been elinated. Comments
  // *** should also be eliminated and performance tuning performed as
  // *** possible.
  // ***
  // **************************************************************************
  
  // Exit now if this label has no children
  if( !toggleLabel.hasChildren ) return;
   
  // Toggle this label's collapse status
  toggleLabel._isCollapsed = (typeof overrideIsCollapsed=="undefined")? !toggleLabel._isCollapsed : overrideIsCollapsed;

  // Change the collapse image
  if( toggleLabel._isCollapsed ) {
    elTreeImage.src = "chrome://gtdinbox/content/collapsetree-closed.gif";
    elTreeImage.alt = '+';
  } else {
    elTreeImage.src = "chrome://gtdinbox/content/collapsetree-open.gif";
    elTreeImage.alt = '-';
  }
    
  // Prepare a variable for the loop
  var elLabel = elToggleLabel;
  
  // Check each next sibling until we run out
  while( elLabel = elLabel.nextSibling ){
    var label = this._labelObjectsHash[ elLabel.getAttribute("gtdLabelFullName") ];
    var hideThisLabel = false;

    // Check to see if the next sibling is still within toggleLabel's tree
    var parent = label;
    if( parent ){
		while ( parent = parent.parent ){
			hideThisLabel = ( hideThisLabel || parent._isCollapsed );
			if( parent == toggleLabel ) break;
		}
	}

    // This label is INSIDE the parent's tree - set display of elLabel
    if ( parent ) elLabel.style.display = ( hideThisLabel ? 'none' : '' );
      
    // This label is OUTSIDE the parent's tree - quit searching!!!
    else break;
  }

}catch(e){
	if( GTDDEBUGMODE ) alert("Label toggleLabelsTreeNode\n" + e.toString());
}
};

Labels.prototype.addNewLabel = function(labelname){
	// Hack-job. Used if new label created; but Gmail doesn't update fast enough. Called by thread.addLabel.

	var elRow = this.doc.createElement("TR");
	elRow.innerHTML = "<td><div><span>" + labelname + "</span></div></td>";
	var elName = elRow.getElementsByTagName("SPAN")[0];
	
	this._tmpLabelNames.push({'labelname':labelname,'elRow':elRow,'elName':elName});
	this.load(null, this._tmpLabelNames);
	
	this.eventHandlers.broadcast("refresh-labels");
};

Labels.prototype.getLabelsEl = function(type){
  var elLabelsClone = this._labelsEls[type]? this._labelsEls[type].cloneNode(true) : null
  if( elLabelsClone ) this.eventObservers.observe( elLabelsClone, "mousedown", this.labelsSectionMousedown, false);
	return elLabelsClone;
};
Labels.prototype.getLabels = function(type){
	return this._labels[type] || {};
};
Labels.prototype.getLabelsArray = function(types){
	if( !types ) types = ["_all"];
	else if( typeof types=="string" ){
		types = [types];
	}
	
	if( types.length==1 ){
		return this._labelsArray[types[0]] || [];
	}else{
		var a = [];
		for( var i = 0; i < types.length; i++ ){
			if( this._labelsArray[types[i]] ) a = a.concat( this._labelsArray[types[i]] );
		}
		return a;
	}
};
Labels.prototype.getLabelsHash = function(types){
	if( !types ) types = ["_all"];
	else if( typeof types=="string" ) types = [types];
	
	if( types.length==1 ){
		return this._labelsHash[types[0]] || {};
	}else{
		var hash = {};
		var o = null;
		for( var i = 0; i < types.length; i++ ){
			if( o = this._labelsHash[types[i]] ){
				for( p in o ) if( o.hasOwnProperty(p) ) hash[p] = o[p];
			}
		}
		return hash;
	}
};
Labels.prototype.hasLabel = function(labelname){
	var labels = this._labelsArray["_all"];
	for( var i = 0; i < labels.length; i++ ){
		if( labels[i]==labelname ) return true;
	}
	return false;
};

Labels.prototype.getLabelsElForProjectShortList = function(options){
	// Calculate projects that are likely to be needed
	// Criteria:
	//		Not currently selected
	//		Any projects that match the message body
	//		3 projects from labels recently applied to the names
	//		3 projects from labels recently applied
	// Then show recommended box.
try{
	
	var pprefix = this.settings.labels.types.Projects.prefix;
	
	var rootProjects = {labels:{}, found:0};
	var fAddLabel = function(rootProjects, label){
		var splitName = label.replace(pprefix, "").split("/");
		if( !rootProjects.labels[splitName[splitName.length-1]] ) rootProjects.found++;
		for( var i = 0; i < splitName.length; i++ ){
			if( !rootProjects.labels[splitName[i]] ) rootProjects.labels[splitName[i]] = true;
		}
		/*
		if( !rootProjects.labels[label] ){
			rootProjects.labels[label] = label;	
			rootProjects.found++;
		}
		*/
	};
	
	// Include projects already selected
	if( options.thread && (options.maxSelected||0)>0 ){
		var i = rootProjects.found;
		var selected = options.thread.labels;
		for( var j = 0; j < selected.length; j++ ){
			var l = selected[j];
			if( l.indexOf(pprefix)==0 ) fAddLabel(rootProjects, l);
			if( (rootProjects.found-i)>options.maxSelected ) break;
		}
	}
	
	if( options.thread && (options.maxBody||0)>0 ){
		// Process labels related to message:
		var labels = this.getLabelsArray("Projects");
		if( labels ){
			labels = options.thread.testLabels( labels, pprefix );
			
			var i = rootProjects.found;
			for( var i = 0; i < labels.length; i++ ){
				if( !options.selected || !options.selected[labels[i]] ) fAddLabel(rootProjects, labels[i]);
				if( (rootProjects.found-i)>options.maxBody ) break;
			}
		}
	}
	
	// Retrieve recent and associated with names on this thread
	if( options.thread && (options.maxRecentByName||0)>0 ){
		var names = options.thread.getNames();
		var contacts = [];
		for( var i = 0; i < names.length; i++ ) contacts.push( names[i][1] );
		var projects = this.gtdinboxPage.sp.getRecentProjectsForContacts(contacts);
		
		var i = rootProjects.found; 
		for( var i = 0; i < projects.length; i++ ){
			if( (rootProjects.found-i)<=options.maxRecentByName ) fAddLabel(rootProjects, projects[i]);
		}
		
		//var fRow = function(statement){
		//	if( (rootProjects.found-i)<=options.maxRecentByName ) fAddLabel(rootProjects, statement.getString(0));
		//};
		//this.gtdinboxPage.db.select("SELECT DISTINCT label FROM project_label_activity WHERE " + where + " ORDER BY date DESC LIMIT 20", fRow);
	}
	
	if( (options.maxRecent||0)>0 ){
		// Retrieve recent in general
		var i = rootProjects.found;
		var projects = this.gtdinboxPage.sp.getRecentlyAttachedProjects();
		for( var i = 0; i < projects.length; i++ ){
			if( (rootProjects.found-i)<=options.maxRecent ) fAddLabel(rootProjects, projects[i]);
		}
		//var fRow = function(statement){
		//	if( (rootProjects.found-i)<=options.maxRecent ) fAddLabel(rootProjects, statement.getString(0));
		//};
		//this.gtdinboxPage.db.select("SELECT DISTINCT label FROM project_label_activity ORDER BY date DESC LIMIT 20", fRow);
	}
	
	// Now go through labels, stripping out any 'root' elements that are not allowed
	var el = this.getLabelsEl("Projects");
	

	var els = el.getElementsByTagName("*");
	for( var i = els.length - 1; i>=0; i-- ){
		if( els[i].className=="title" ) els[i].innerHTML = this.gtdinboxPage.stringBundle.getString("Labels.RecentProjects");	
		if( els[i].getAttribute("gtdName") && !rootProjects.labels[els[i].getAttribute("gtdName")] ){
			els[i].parentNode.removeChild(els[i]);
		}
	}
		
	return el;
	
}catch(e){
	if( GTDDEBUGMODE ) alert("labels createShortList\n" + e.toString());
}
};

/*
Labels.prototype.mouseDown = function(event){
	var el = GIPROTOTYPE.element(event);
	if( el.className.indexOf("filter-popuper")==-1 ) return;
	
try{
	GIPROTOTYPE.stop(event);
	
	if( this._filterChild ){
		this._filterChild.destroy();
		if( this._filterChildEl==el ) return;
	}
	var elSpan = el.parentNode.getElementsByTagName("SPAN")[0];
	var labelname = elSpan.getAttribute("gtdLabelName");
	var type = elSpan.getAttribute("gtdLabelType");
	var f = this.settings.filters[type];
	this._filterChild = new FilterPopupUI( this.gtdinboxPage ).create(el.ownerDocument.body, GIPROTOTYPE.searchableLabel(labelname), true, true, f.inlineTypes, f.extendTypes);
	this._filterChild.popup(el,{location:'right'});
	this._filterChildEl = el;
}catch(e){
	if( GTDDEBUGMODE ) alert("labels mousedown\n" + e.toString());
}
};
*/









/* ENVIRONMENT EVENTS */

Labels.prototype.viewChange = function(gmail){
	// Register event handlers with the actioncombo & alert box
	
try{
	
	if( this.elAlertBox ) this.eventObservers.stopObserving( this.elAlertBox, "DOMNodeInserted", this.alertBoxChange, false );
	this.elAlertBox = this.gtdinboxPage.gmailDOM.element("alert-box");
	this.eventObservers.observe( this.elAlertBox, "DOMNodeInserted", this.alertBoxChange, false );
	
	
	if( this._elActionCombo ){
		this.eventObservers.stopObserving( this._elActionCombo, "click", this.actionComboClick, false );
		this.eventObservers.stopObserving( this._elActionCombo, "mousedown", this._restoreScrollTop, true);
	}
	this._elActionCombo = this.gtdinboxPage.gmailDOM.element("action-combo");
	this.eventObservers.observe( this._elActionCombo, "click", this.actionComboClick, false );
	this.eventObservers.observe( this._elActionCombo, "mousedown", this._restoreScrollTop, true);

}catch(e){
	if( GTDDEBUGMODE ) alert("labels viewChange\n" + e.toString());
}
};
Labels.prototype.actionComboClick = function(event){
	var elActionList = this.gtdinboxPage.gmailDOM.element("action-combo-list");
	if( !elActionList ) return;
	
	if( this._elActionCombo ) this.eventObservers.stopObserving( this._elActionCombo, "click", this.actionComboClick, false );
	this._elActionList = elActionList;
	this.eventObservers.observe( this._elActionList, "click", this.actionListClick, true );
};


Labels.prototype.actionListClick = function(event){
	// Should only capture manual clicks - then broadcast it
try{	
	// Find the element clicked 
	var el = GIPROTOTYPE.element(event);
	var label = el.innerHTML.replace(/<.+?>/g,"");
	var safelabel = label.replace(/\"/g, "\\\"");
	var act = el.getAttribute("act") || 0;
	if( !safelabel || !(act==12 || act==13) ){
		return;
	}

	this.eventHandlers.broadcast("actionlist-manual-click", {'label':label, 'act':act, 'safelabel':safelabel});
	return;
}catch(e){
	if( GTDDEBUGMODE ) alert("labels actionComboList\n" + e.toString());
}
};
















/* ENVIRONMENT ACTIONS */

Labels.prototype.labelThread = function(options, qItem){
	// Inform Gmail of change
		
try{
	
	if( !qItem ){	// no callback
		var qItem = {};
		qItem.options = options;
		qItem.start = {'callee':this, 'callback':this.labelThread, args:[qItem.options, qItem]};
		/*
		qItem.start = GIPROTOTYPE.bind(this, function(qItem){
				this.labelThread(qItem.options, qItem);
			});
		*/
		qItem.callback = options.fCallback;
		qItem.test = GIPROTOTYPE.bind(this, function(qItem){
			try{
				var msg = this.elAlertBox.innerHTML;
				if( msg==this.elAlertBox_LastMsg ) return;
				if( msg.indexOf( "\"" + qItem.options.labelname + "\"" )>-1 ){
					this.elAlertBox_LastMsg = msg;
					return true;
				}
				}catch(e){
					if( GTDDEBUGMODE ) alert("Labels labelThread test\n" + e.toString());
				}
			});
		this._createQueueItem(qItem);
		return;
	}


	var labelname = options.labelname;
	var on = options.on;
	var threads = options.threads;
	
	if( !(threads instanceof Array) ) threads = [threads];

	
	var result = {act:0, elAction:null};
	var fLI = GIPROTOTYPE.bind(this, function(el){
			if( el.innerHTML.replace(/<.+?>/g, "")==labelname ){
				result.act = el.getAttribute("act");
				result.elAction = el;
				if( el.getAttribute("act")=="13" ) return true;	// remove label overrides (remove & add can be on same label)
			}
		});
	this._doMoreAction(fLI, false, true);
	var elAction = result.elAction;
	var act = result.act;
	

	
	if( elAction && ((typeof on=="undefined") || (act==12 && on) || (act==13 && !on)) ){
		if( options.fPreAdd ) options.fPreAdd();
		
		GIPROTOTYPE.doMouseEvent(this.doc, elAction, "mouseup");
				
		if( act==12 ){
			for( var i = 0; i < threads.length; i++ ) threads[i].addLabel(labelname);	
		}else{
			for( var i = 0; i < threads.length; i++ ){
				threads[i].removeLabel(labelname);
			}
		}
		options.act = act;
		setTimeout( GIPROTOTYPE.bind(this, function(){this._testQueue()}), 2000 );	// call to make sure deadwood is cleared
	}else{
		options.act = 0;
		if( !elAction ) this._clickActionCombo();	// turn it off
		this._clearQueueItem(qItem);
	}
		
}catch(e){
	if( GTDDEBUGMODE ) alert("labels labelThread\n" + e.toString());
}
	
	
		
};


Labels.prototype.alertBoxChange = function(event){
	this._multiToOne.process("alert-box", GIPROTOTYPE.bind(this, function(){
		this._testQueue();
	}));
	return;

};

/*
Labels.prototype.labelThreadCallback = function(labelname, testToClearOnly){
	// Called after ajax response; and timeout period
	if( !this._labelThreadInProgress ) return;
	
	var callbacks = [];	// queue callbacks due to time senstive processing
	var d = (new Date()).valueOf() - 1000;	// TODO correct to 1000
	for( var i = 0; i < this._labelThreadInProgress.length; i++ ){
		if( (labelname && this._labelThreadInProgress[i].labelname==labelname) ||
			(!labelname && this._labelThreadInProgress[i].date < d)
			){
				for( var j = i; j>=0; j-- ){
					if( this._labelThreadInProgress[j].fCallback ){
						callbacks.push( [this._labelThreadInProgress[j].fCallback, this._labelThreadInProgress[j].act] );
					}
					this._labelThreadInProgress.splice(j,1);
				}
		}
	}
	for( var i = 0; i < callbacks.length; i++ ) 	callbacks[i][0]( callbacks[i][1] );	// process callbacks
	
	// Tell it that it may continue processing
	if( !testToClearOnly ) this.labelThread(null, true);
	
	if( this._labelThreadInProgress.length==0 ) this.eventHandlers.broadcast("label-thread-unlocked");
};
*/

Labels.prototype.createLabel = function(qItem){

	if( !qItem ){	// no callback
		var qItem = {};
		qItem.start = {'callee':this, 'callback':this.createLabel, args:[qItem]};
		/*
		qItem.start = GIPROTOTYPE.bind(this, function(qItem){
				this.createLabel(qItem);
			});
		*/
		this._createQueueItem(qItem);
		return;
	}

	var fLI = GIPROTOTYPE.bind(this, function(el){
			if( el.getAttribute("act")=="14" ){
				GIPROTOTYPE.doMouseEvent(this.doc, el, "mouseup");
				return true;
			}
		});
	this._doMoreAction(fLI);
	
	
	this._clearQueueItem(qItem);
};
Labels.prototype.createLabelAjax = function(labelname){
try{
	if( !labelname ) labelname = prompt(this.gtdinboxPage.stringBundle.getString("Labels.CreateLabel"));	
	if( !labelname ) return;
	
	var finalCallback = GIPROTOTYPE.bind(this, function(){
			this.addNewLabel(labelname);
		});
	this.gtdinboxPage.gmailDOM.addLabel(labelname, null, finalCallback);
	
	

}catch(e){
	if( GTDDEBUGMODE ) alert("labels createlabelajax\n" + e.toString());
}
};

Labels.prototype.getActiveLabels = function(callee, callback, qItem){
try{
	if( !qItem ){	// no callback
		var qItem = {};
		qItem.options = {'callee':callee, 'callback':callback};	// external
		qItem.start = {'callee':this, 'callback':this.getActiveLabels, args:[null, null, qItem]};
		/*
		qItem.start = GIPROTOTYPE.bind(this, function(qItem){
				this.getActiveLabels(callee, callback, qItem);
			});
		*/
		this._createQueueItem(qItem); 
		return; 
	}
	
	
	var result = {labels:[]};
	var fLI = GIPROTOTYPE.bind(this, function(el){
			if( el.getAttribute("act")=="13" ){	// remove a label - means its applied
				result.labels.push( el.innerHTML.replace(/<.+?>/g,"") );
			}
		});
	this._doMoreAction(fLI, true);
		
	// Now callback
	if( qItem.options.callback ) qItem.options.callback.call(qItem.options.callee, result.labels);
	//if( qItem.options.callback ) qItem.options.callback(result.labels);
	//return result.labels;
	
	this._clearQueueItem(qItem);
}catch(e){
	if( GTDDEBUGMODE ) alert("labels getactivelabels\n" + e.toString());
}
};


/*
Labels.prototype.executeActionList = function(f){
	var id = (new Date()).valueOf();
	this._actionListExecId = id;
	
	var fExec = GIPROTOTYPE.bind(this, function(){
			if( id!=this._actionListExecId ) return;
			f();
		});
	
	
};
*/

Labels.prototype._doMoreAction = function(f, forceOff, forceRemainOn){
try{
	this._elActionCombo = this.gtdinboxPage.gmailDOM.element("action-combo");
	this._clickActionCombo();
	this._elActionList = this.gtdinboxPage.gmailDOM.element("action-combo-list");
	if( this._elActionList ){
		this._elActionList.style.display = "none";
		
		var found = false;
		var children = this._elActionList.childNodes;
		for( var i = 0; i < children.length; i++ ){
			var el = children[i];
			if( f(el) ){
				found = true;
				break;
			}
		}
		
		if( (!found || forceOff) && !forceRemainOn ) this._clickActionCombo();	// turn it off
	}
}catch(e){
	if( GTDDEBUGMODE ) alert("Labels _doMoreAction\n" + e.toString());
}
};
Labels.prototype._clickActionCombo = function(){
	this._currentScrollTop = this.gtdinboxPage.gmailDOM.element("document").documentElement.scrollTop;
	GIPROTOTYPE.doMouseEvent(this.doc, this._elActionCombo, "mousedown");
};
Labels.prototype._restoreScrollTop = function(event){
	if( isNaN(this._currentScrollTop) ) return;
	this.gtdinboxPage.gmailDOM.element("window").scrollTo(0, this._currentScrollTop);
	this._currentScrollTop = null;	// make sure it doesn't start jumping after this
};


Labels.prototype._createQueueItem = function(qItem){
try{
	// qItem expected to have {start, test, callback} at least	
	qItem.callee = qItem.callee || this;

	
	this._labelQueue.push(qItem);
	this._testQueue();
}catch(e){
	if( GTDDEBUGMODE ) alert("Labels _createQueueItem\n" + e.toString());
}
};
Labels.prototype._clearQueueItem = function(qItem){
try{
	for( var i = this._labelQueue.length - 1; i>=0; i-- ){
		if( this._labelQueue[i]==qItem ){
			if( this._labelQueue[i].callback ) this._labelQueue[i].callback();
			this._labelQueue.splice(i, 1);
		}
	}
	if( this._labelQueueItem==qItem ){
		this._labelQueueItem = null;
		this._labelQueueLastClear = (new Date()).valueOf();
	}
	this._tryNextQueueItem();
}catch(e){
	if( GTDDEBUGMODE ) alert("Labels _clearQueueItem\n" + e.toString());
}
};
Labels.prototype._testQueue = function(){
try{
	var d = (new Date()).valueOf() - 1500;
	var toClear = [];
	
	var qItem = this._labelQueueItem;
	if( qItem && qItem.test && qItem.test(qItem) ){
		toClear.push(qItem);
	}
	
	for( var i = 0; i < this._labelQueue.length; i++ ){
		var qItem = this._labelQueue[i];
		if( qItem.date < d ){
			toClear.push(qItem);
		}
	}
	for( var i = 0; i < toClear.length; i++ ) this._clearQueueItem(toClear[i]);
	this._tryNextQueueItem();
}catch(e){
	if( GTDDEBUGMODE ) alert("Labels _testQueue\n" + e.toString());
}
};
Labels.prototype._tryNextQueueItem = function(){
try{
	if( !this._labelQueueItem && this._labelQueue.length > 0 ){
		this._labelQueueItem = this._labelQueue[0];
		this._labelQueueItem.date = (new Date()).valueOf();
		//this._labelQueueItem.start(this._labelQueueItem);
		if( this._labelQueueItem.start ){
			var qItem = this._labelQueueItem;
			var d = (new Date()).valueOf() - 300;
			if( (this._labelQueueLastClear||0) > d ){	// Something finished recently
				setTimeout( function(){	
					qItem.start.callback.apply( qItem.start.callee, qItem.start.args );
					}, 300);
			}else{
				qItem.start.callback.apply( qItem.start.callee, qItem.start.args );
			}
		}
	}
}catch(e){
	if( GTDDEBUGMODE ) alert("Labels _tryNextQueueItem\n" + e.toString());
}
};








Labels.prototype.destroy = function(){
	for( var i = 0; i < this._showHiders.length; i++ ){
		this._showHiders[i].destroy();
	}
	this._multiToOne.destroy();
	this._showHiders.length = 0;
	this.eventObservers.destroy();
};